<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>自动换盘 · 3MF合并工具（精简版）</title>
  <link rel="icon" href="data:," />
  <!-- 依赖：JSZip & SparkMD5 -->
  <script src="https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/spark-md5@3.0.2/spark-md5.min.js"></script>
  <style>
    :root{
      --bg:#0a0f1a;
      --card:#11182b;
      --card-hover:#152036;
      --muted:#8ea0bf;
      --accent:#4aa8ff;
      --ok:#39d98a;
      --warn:#ffb020;
      --err:#ff6b6b;
      --transition: all .25s ease;
    }
    *{box-sizing:border-box} 
    body{
      margin:0;
      font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Inter,PingFang SC,Microsoft YaHei,Arial;
      color:#e6eefc;
      background:linear-gradient(160deg,#0b1020,#0a0f1a);
      background-attachment: fixed;
    }
    header{
      display:flex;
      align-items:center;
      justify-content:space-between;
      padding:16px 20px;
      border-bottom:1px solid #1a233a;
      background:#0c1326;
      position:sticky;
      top:0;
      z-index:10;
      box-shadow:0 4px 20px rgba(0,0,0,0.3);
    }
    header h1{font-size:18px;margin:0;color:#eaf2ff}
    header small{color:var(--muted)}
    main{padding:18px;display:grid;gap:18px;grid-template-columns: 360px 1fr}
    .card{
      background:var(--card);
      border:1px solid #1c2742;
      border-radius:14px;
      padding:14px;
      box-shadow:0 10px 30px rgba(0,0,0,.25);
      transition:var(--transition);
    }
    .card:hover{
      box-shadow:0 15px 35px rgba(0,0,0,.35);
      border-color:#2d3b66;
    }
    .drop{
      border:1px dashed #304061;
      border-radius:12px;
      padding:22px;
      text-align:center;
      color:var(--muted);
      cursor:pointer;
      transition:var(--transition);
    }
    .drop:hover{
      border-color:var(--accent);
      color:#cfe3ff;
      background:#0f1a31;
    }
    .drop.dragover{
      border-color:var(--ok);
      background:#0f1a31;
      transform: translateY(-2px);
    }
    .muted{color:var(--muted)}
    .row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
    .btn{
      appearance:none;
      border:1px solid #2a3860;
      background:#0f1a31;
      color:#e6eefc;
      border-radius:10px;
      padding:10px 14px;
      cursor:pointer;
      transition:var(--transition);
    }
    .btn:hover{
      background:#142040;
      border-color:#35548a;
      transform: translateY(-1px);
    }
    .btn.primary{
      background:linear-gradient(135deg,#1e3b72,#274a8c);
      border-color:#35548a;
    }
    .btn.primary:hover{
      background:linear-gradient(135deg,#274a8c,#35548a);
      transform: translateY(-2px);
      box-shadow:0 4px 12px rgba(74,168,255,0.2);
    }
    .btn:disabled{opacity:.5;cursor:not-allowed}
    input[type="number"], input[type="text"]{
      background:#0f1a31;
      border:1px solid #2a3860;
      border-radius:10px;
      padding:8px 10px;
      color:#e6eefc;
      transition:var(--transition);
    }
    input[type="number"]:focus, input[type="text"]:focus{
      outline:none;
      border-color:var(--accent);
      box-shadow:0 0 0 2px rgba(74,168,255,0.2);
    }
    table{width:100%;border-collapse:separate;border-spacing:0 8px}
    th,td{
      text-align:left;
      font-size:14px;
      padding:10px 12px;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    thead th{color:#bcd0f0;font-weight:600}
    tbody tr{
      background:#0f1830;
      border:1px solid #26365b;
      transition:var(--transition);
    }
    tbody tr:hover{
      background:var(--card-hover);
      border-color:#35548a;
      transform: translateX(4px);
    }
    tbody tr.selected{
      background:#152a5a;
      border-color:#35548a;
    }
    tbody tr td:first-child{border-radius:10px 0 0 10px}
    tbody tr td:last-child{border-radius:0 10px 10px 0}
    
    /* 表格列宽设置 */
    table th:nth-child(3), table td:nth-child(3) {width: 200px;}
    table th:nth-child(4), table td:nth-child(4) {width: 180px;}
    table th:nth-child(5), table td:nth-child(5) {width: 120px;}
    table th:nth-child(6), table td:nth-child(6) {width: 100px;}
    table th:nth-child(7), table td:nth-child(7) {width: 220px;}
    
    .tag{
      display:inline-flex;align-items:center;gap:6px;
      background:#12213c;border:1px solid #2a3860;color:#bcd0f0;
      border-radius:999px;padding:6px 10px;margin:4px 6px 0 0;
      transition:var(--transition);
    }
    .tag:hover{
      background:#1a305c;
      border-color:#35548a;
      transform: translateY(-1px);
    }
    .stat{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
    .stat .item{
      background:#0f1830;border:1px solid #26365b;
      border-radius:12px;padding:12px;
      transition:var(--transition);
    }
    .stat .item:hover{
      background:var(--card-hover);
      border-color:#35548a;
      transform: translateY(-2px);
    }
    .mono{font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace}
    footer{padding:16px 20px;color:#8ea0bf}
    .hint{font-size:12px;color:#97a9c9}
    .err{color:var(--err)}
    
    /* 缩略图样式 */
    .thumbnails {
      display: flex;
      flex-wrap: wrap;
      gap: 15px;
      margin-top: 14px;
      max-height: 300px;
      overflow-y: auto;
      padding: 12px;
      border-radius: 8px;
      background: #0f1830;
    }
    .thumbnail-item {
      width: 100px;
      height: 120px;
      position: relative;
      transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
      cursor: grab;
    }
    .thumbnail-item:active {
      cursor: grabbing;
    }
    .thumbnail-item:hover {
      transform: translateY(-3px) scale(1.02);
      box-shadow: 0 8px 16px rgba(0,0,0,0.3);
    }
    .thumbnail-item.dragging {
      opacity: 0.7;
      transform: scale(1.05);
      z-index: 10;
      transition: none;
    }
    .thumbnail-item img {
      width: 100%;
      height: 90px;
      object-fit: contain;
      border-radius: 6px;
      border: 1px solid #26365b;
      transition: var(--transition);
    }
    .thumbnail-item:hover img {
      border-color: var(--accent);
    }
    .thumbnail-remove {
      position: absolute;
      top: -5px;
      right: -5px;
      background: var(--err);
      border-radius: 50%;
      width: 20px;
      height: 20px;
      display: flex;
      align-items: center;
      justify-content: center;
      cursor: pointer;
      font-size: 12px;
      border: 1px solid #26365b;
      transition: var(--transition);
    }
    .thumbnail-remove:hover {
      transform: scale(1.1);
      background: #ff8787;
    }
    .file-name {
      font-size: 12px;
      text-align: center;
      margin-top: 6px;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    
    /* 移动按钮样式 */
    .move-btn {
      background: transparent;
      border: none;
      color: var(--muted);
      cursor: pointer;
      font-size: 16px;
      transition: var(--transition);
      padding: 2px 6px;
      border-radius: 4px;
    }
    .move-btn:hover {
      color: var(--accent);
      background: rgba(74, 168, 255, 0.1);
    }
  </style>
</head>
<body>
  <header>
    <div>
      <h1>自动换盘 · 3MF合并工具（精简版）</h1>
      <small class="muted">拖入 .3mf 或 .gcode.3mf 文件，自动识别托盘与耗材信息</small>
    </div>
    <div class="row">
      <input id="fileName" type="text" placeholder="output_file_name" style="width:220px" />
      <button id="btnExport" class="btn primary" disabled>导出合并后的 .3mf</button>
    </div>
  </header>

  <main>
    <section class="card">
      <div id="drop" class="drop">
        <div>将切片文件拖到这里，或 <u>点击选择</u></div>
        <div class="hint" style="margin-top:8px">支持 *.3mf, *.gcode.3mf</div>
        <input id="file" type="file" accept=".3mf,.gcode.3mf" hidden multiple />
      </div>
      
      <!-- 已添加文件（支持拖拽排序） -->
      <div style="margin-top:14px">
        <div class="muted" style="margin-bottom:6px">已添加文件（可拖拽调整顺序）</div>
        <div id="thumbnails" class="thumbnails"></div>
      </div>

      <div style="margin-top:14px">
        <label class="muted">循环次数</label>
        <input id="loops" type="number" min="0" value="1" style="width:100px;margin-left:8px" />
        <div class="hint" style="margin-top:4px">输入0可清空当前值，再输入新数值</div>
      </div>
      <div style="margin-top:14px">
        <div class="muted" style="margin-bottom:6px">耗材统计</div>
        <div id="filamentTags"></div>
      </div>
      <div style="margin-top:14px">
        <div class="muted" style="margin-bottom:6px">导出说明</div>
        <div class="hint">本页为参考实现，导出时会将选中的托盘 G-code 进行顺序拼接，并写入新的 <span class="mono">Metadata/plate_1.gcode</span>；部分高级字段未完全重建（如 AMS/逐层自定义 G-code），但可用于原型验证与小批量合并。</div>
      </div>
      <div id="err" class="err" style="margin-top:10px"></div>
    </section>

    <section class="card">
      <div class="stat">
        <div class="item">
          <div class="muted">总时长</div>
          <div id="totalTime" class="mono" style="font-size:20px;margin-top:4px">-</div>
        </div>
        <div class="item">
          <div class="muted">平均时长/个</div>
          <div id="avgTime" class="mono" style="font-size:20px;margin-top:4px">-</div>
        </div>
        <div class="item">
          <div class="muted">托盘数量</div>
          <div id="plateCount" class="mono" style="font-size:20px;margin-top:4px">-</div>
        </div>
        <div class="item">
          <div class="muted">循环×托盘</div>
          <div id="loopPlates" class="mono" style="font-size:20px;margin-top:4px">-</div>
        </div>
      </div>

      <div style="margin-top:14px" class="row">
        <button id="btnSelectAll" class="btn">全选</button>
        <button id="btnClearAll" class="btn">全不选</button>
      </div>

      <div style="margin-top:10px;overflow:auto">
        <table>
          <thead>
            <tr>
              <th style="width:40px"></th>
              <th style="width:60px">打印顺序</th>
              <th>托盘</th>
              <th>来源文件</th>
              <th>预计时长</th>
              <th>重复</th>
              <th>耗材槽位</th>
              <th style="width:80px">调整</th>
            </tr>
          </thead>
          <tbody id="tbody"></tbody>
        </table>
      </div>
    </section>
  </main>

  <footer>
    <div class="hint">UI/交互借鉴了你提供的 <code>huanpan.html</code>（自动换盘程序思想），在此基础上做了精简与结构化重构，便于后续二开。</div>
  </footer>

  <script>
    // —— 工具方法 ——
    const $ = (sel, root=document) => root.querySelector(sel);
    const $$ = (sel, root=document) => Array.from(root.querySelectorAll(sel));
    const fmtHMS = s => { if(!Number.isFinite(s)) return '-'; const d=Math.floor(s/86400); s%=86400; const h=Math.floor(s/3600); s%=3600; const m=Math.floor(s/60); const ss=Math.floor(s%60); return (d?d+'d ':'')+String(h).padStart(2,'0')+':'+String(m).padStart(2,'0')+':'+String(ss).padStart(2,'0'); };
    const parseDuration = (str='') => {
      str = String(str).trim();
      if(/^\d+:\d+:\d+$/.test(str)){ const [h,m,s]=str.split(':').map(Number); return h*3600+m*60+s; }
      let sec = 0; (str.match(/(\d+)\s*[dhms]/ig)||[]).forEach(tok=>{const v=+tok.replace(/\D/g,''); if(/d/i.test(tok)) sec+=v*86400; else if(/h/i.test(tok)) sec+=v*3600; else if(/m/i.test(tok)) sec+=v*60; else if(/s/i.test(tok)) sec+=v;});
      return sec||0;
    };

    // —— 状态 ——
    let fileZips = new Map();
    let plates = [];
    let selected = new Set();

    const els = {
      drop: $('#drop'), file: $('#file'), tbody: $('#tbody'),
      totalTime: $('#totalTime'), avgTime: $('#avgTime'), plateCount: $('#plateCount'), loopPlates: $('#loopPlates'),
      loops: $('#loops'), filamentTags: $('#filamentTags'), fileName: $('#fileName'),
      btnExport: $('#btnExport'), btnSelectAll: $('#btnSelectAll'), btnClearAll: $('#btnClearAll'), 
      err: $('#err'), thumbnails: $('#thumbnails')
    };

    // —— 事件绑定 ——
    els.drop.addEventListener('click', ()=> els.file.click());
    els.drop.addEventListener('dragover', e=>{ 
      e.preventDefault(); 
      e.stopPropagation();
      els.drop.classList.add('dragover'); 
    });
    els.drop.addEventListener('dragleave', ()=> els.drop.classList.remove('dragover'));
    els.drop.addEventListener('drop', e=>{ 
      e.preventDefault(); 
      e.stopPropagation();
      els.drop.classList.remove('dragover'); 
      const files=[...e.dataTransfer.files]; 
      if(files.length) handleFiles(files); 
    });
    els.file.addEventListener('change', e=> handleFiles([...e.target.files]));

    // 拖拽排序相关变量
    let draggedItem = null;
    let draggedIndex = -1;

    // 初始化拖拽排序
    function initDragSort() {
      const thumbnailsContainer = els.thumbnails;
      
      thumbnailsContainer.addEventListener('dragstart', (e) => {
        if (e.target.closest('.thumbnail-item')) {
          draggedItem = e.target.closest('.thumbnail-item');
          draggedIndex = Array.from(thumbnailsContainer.children).indexOf(draggedItem);
          draggedItem.classList.add('dragging');
          e.dataTransfer.effectAllowed = 'move';
        }
      });
      
      thumbnailsContainer.addEventListener('dragover', (e) => {
        e.preventDefault();
        if (!draggedItem) return;

        const target = e.target.closest('.thumbnail-item');
        if (!target || target === draggedItem) return;

        const rect = thumbnailsContainer.getBoundingClientRect();
        const y = e.clientY - rect.top;
        const targetRect = target.getBoundingClientRect();
        const targetY = targetRect.top - rect.top + targetRect.height / 2;

        if (y < targetY) {
          thumbnailsContainer.insertBefore(draggedItem, target);
        } else {
          thumbnailsContainer.insertBefore(draggedItem, target.nextSibling);
        }
      });
      
      thumbnailsContainer.addEventListener('dragend', () => {
        if (draggedItem) {
          draggedItem.classList.remove('dragging');
          
          // 更新文件顺序
          const newOrder = Array.from(thumbnailsContainer.children).map(item => item.dataset.file);
          const reorderedPlates = [];
          const reorderedFileZips = new Map();
          
          newOrder.forEach(fileName => {
            const fileData = fileZips.get(fileName);
            if (fileData) {
              reorderedFileZips.set(fileName, fileData);
              // 找到该文件的所有托盘
              const filePlates = plates.filter(plate => plate.sourceFile === fileName.split('_').slice(0, -1).join('_'));
              reorderedPlates.push(...filePlates);
            }
          });
          
          fileZips = reorderedFileZips;
          plates = reorderedPlates;
          
          // 重置选中状态
          selected.clear();
          
          render();
          draggedItem = null;
          draggedIndex = -1;
        }
      });
    }

    async function handleFiles(files){
      for(const f of files){
        try{
          if(!/\.3mf($|\b)/i.test(f.name)) {
            showErr(`文件 ${f.name} 不支持，仅支持 *.3mf / *.gcode.3mf 文件`);
            continue;
          }

          const zip = await JSZip.loadAsync(f);
          const modelCfg = await zip.file('Metadata/model_settings.config')?.async('text');
          const sliceInfo = await zip.file('Metadata/slice_info.config')?.async('text');
          if(!modelCfg || !sliceInfo) {
            showErr(`文件 ${f.name} 未找到切片元数据`);
            continue;
          }

          // 使用唯一键避免覆盖
          const uniqueKey = `${f.name}_${Date.now()}`;
          const fileObj = { zip, file: f };
          fileZips.set(uniqueKey, fileObj);
          
          const parser = new DOMParser();
          const modelXML = parser.parseFromString(modelCfg,'text/xml');
          const sliceXML = parser.parseFromString(sliceInfo,'text/xml');

          const plateNodes = [...modelXML.getElementsByTagName('plate')];
          const filePlates = [];
          
          for(let i=0;i<plateNodes.length;i++){
            const plate = plateNodes[i];
            const metas = [...plate.querySelectorAll('metadata')].reduce((acc,m)=>{acc[m.getAttribute('key')] = m.getAttribute('value'); return acc;},{});
            const gcodePath = metas['gcode_file'];
            if(!gcodePath) continue;

            const gcodeText = await zip.file(gcodePath)?.async('string');
            let timeStr = '-'; let timeSec = 0;
            if(gcodeText){
              const idx = gcodeText.indexOf('total estimated time: ');
              if(idx>=0){
                const line = gcodeText.slice(idx+22, gcodeText.indexOf('\n', idx)).trim();
                timeStr = line; timeSec = parseDuration(line);
              }
            }

            const indexMeta = plate.querySelector('[key="index"]');
            const indexVal = indexMeta ? indexMeta.getAttribute('value') : String(i+1);
            const plateInSlice = sliceXML.querySelector(`[key="index"][value="${indexVal}"]`);
            const filRoot = plateInSlice ? plateInSlice.parentElement : sliceXML.getElementsByTagName('plate')[0];
            const filNodes = filRoot?.getElementsByTagName('filament') || [];
            const filaments = [...filNodes].map(n=>({
              id: n.getAttribute('id'),
              type: n.getAttribute('type'),
              used_m: Number(n.getAttribute('used_m')||0),
              used_g: Number(n.getAttribute('used_g')||0),
              color: n.getAttribute('color')||'#64748b'
            }));

            filePlates.push({
              name: `${metas['plater_name'] || `plate_${indexVal}`}_${Date.now().toString().slice(-4)}`,
              gcodePath, 
              timeStr, 
              timeSec, 
              filaments,
              sourceFile: f.name
            });
          }

          plates = [...plates, ...filePlates];
          
          const thumbnailUrl = await loadThumbnail(uniqueKey, zip);
          fileObj.thumbnail = thumbnailUrl;
          
          render();
          
        }catch(err){
          showErr(`处理文件 ${f.name} 出错: ${err.message||String(err)}`);
        }
      }
    }

    async function loadThumbnail(fileName, zip) {
      try {
        const thumbnailFile = zip.file('Metadata/plate_1.png');
        let thumbnailUrl;

        if (thumbnailFile) {
          const imgData = await thumbnailFile.async('blob');
          thumbnailUrl = URL.createObjectURL(imgData);
        } else {
          thumbnailUrl = 'https://via.placeholder.com/150x100?text=No+Thumbnail';
        }

        const thumbItem = document.createElement('div');
        thumbItem.className = 'thumbnail-item';
        thumbItem.draggable = true;
        thumbItem.dataset.file = fileName;
        thumbItem.innerHTML = `
          <img src="${thumbnailUrl}" alt="${fileName} 缩略图">
          <div class="thumbnail-remove" data-file="${fileName}">×</div>
          <div class="file-name">${fileName.split('_').slice(0, -1).join('_')}</div>
        `;

        els.thumbnails.appendChild(thumbItem);

        thumbItem.querySelector('.thumbnail-remove').addEventListener('click', (e) => {
          const fileToRemove = e.target.dataset.file;
          removeFile(fileToRemove);
        });

        return thumbnailUrl;

      } catch (err) {
        console.error('加载缩略图失败:', err);
        const errorUrl = 'https://via.placeholder.com/150x100?text=Error+Loading+Image';

        const thumbItem = document.createElement('div');
        thumbItem.className = 'thumbnail-item';
        thumbItem.draggable = true;
        thumbItem.dataset.file = fileName;
        thumbItem.innerHTML = `
          <img src="${errorUrl}" alt="${fileName} 缩略图加载失败">
          <div class="thumbnail-remove" data-file="${fileName}">×</div>
          <div class="file-name">${fileName.split('_').slice(0, -1).join('_')}</div>
        `;

        els.thumbnails.appendChild(thumbItem);

        thumbItem.querySelector('.thumbnail-remove').addEventListener('click', (e) => {
          const fileToRemove = e.target.dataset.file;
          removeFile(fileToRemove);
        });

        return errorUrl;
      }
    }

    function removeFile(fileName) {
      fileZips.delete(fileName);
      const baseName = fileName.split('_').slice(0, -1).join('_');
      const initialLength = plates.length;
      plates = plates.filter(plate => plate.sourceFile !== baseName);
      
      const removedCount = initialLength - plates.length;
      if (removedCount > 0) {
        const newSelected = new Set();
        selected.forEach(idx => {
          let adjustedIdx = idx;
          plates.forEach((p, i) => {
            if (i < idx && p.sourceFile === baseName) {
              adjustedIdx--;
            }
          });
          if (adjustedIdx >= 0 && adjustedIdx < plates.length) {
            newSelected.add(adjustedIdx);
          }
        });
        selected = newSelected;
      }
      
      const thumbItem = els.thumbnails.querySelector(`[data-file="${fileName}"]`);
      if (thumbItem) {
        els.thumbnails.removeChild(thumbItem);
      }
      
      render();
    }

    function resetState(){
      fileZips.clear();
      plates.length=0; 
      selected.clear();
      els.tbody.innerHTML=''; 
      els.filamentTags.innerHTML='';
      els.thumbnails.innerHTML='';
      els.totalTime.textContent='-'; 
      els.avgTime.textContent='-';
      els.plateCount.textContent='-'; 
      els.loopPlates.textContent='-';
      els.btnExport.disabled=true; 
      els.err.textContent='';
    }

    function showErr(msg){ 
      els.err.textContent=msg;
      setTimeout(() => {
        if (els.err.textContent === msg) {
          els.err.textContent = '';
        }
      }, 3000);
    }

    // 调整托盘顺序
    function movePlate(oldIndex, newIndex) {
      if (newIndex < 0 || newIndex >= plates.length) return;
      
      const [movedPlate] = plates.splice(oldIndex, 1);
      plates.splice(newIndex, 0, movedPlate);
      
      // 调整选中状态
      const wasSelected = selected.has(oldIndex);
      selected.delete(oldIndex);
      
      if (wasSelected) {
        selected.add(newIndex);
      } else {
        if (selected.has(newIndex)) {
          selected.delete(newIndex);
          selected.add(oldIndex);
        }
      }
      
      render();
    }

    function render(){
      els.tbody.innerHTML='';
      plates.forEach((p,idx)=>{
        const tr = document.createElement('tr');
        tr.className = selected.has(idx) ? 'selected' : '';
        tr.innerHTML = `
          <td><input type="checkbox" class="chk" data-i="${idx}" ${selected.has(idx) ? 'checked' : ''}></td>
          <td class="mono">${idx+1}</td>
          <td class="mono">${p.name}<div class="hint">${p.gcodePath}</div></td>
          <td class="hint">${p.sourceFile}</td>
          <td class="mono">${p.timeStr||'-'}</td>
          <td><input type="number" class="rep" data-i="${idx}" min="0" value="1" style="width:80px"></td>
          <td>${p.filaments.map(f=>`<span class="tag" title="g:${f.used_g} / m:${f.used_m}"><span style="width:10px;height:10px;border-radius:50%;display:inline-block;background:${f.color}"></span> 槽 ${f.id} · ${f.type||'-'} · ${(f.used_g || 0).toFixed(2)}g</span>`).join('')}</td>
          <td>
            <button class="move-btn move-up" data-i="${idx}">↑</button>
            <button class="move-btn move-down" data-i="${idx}">↓</button>
          </td>
        `;
        
        // 行点击选中功能
        tr.addEventListener('click', (e) => {
          if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') return;
          const chk = tr.querySelector('.chk');
          chk.checked = !chk.checked;
          chk.dispatchEvent(new Event('change'));
        });
        
        els.tbody.appendChild(tr);
      });

      $$('.chk').forEach(chk=>chk.addEventListener('change', e=>{
        const i = +e.target.dataset.i; 
        if(e.target.checked) {
          selected.add(i);
          els.tbody.children[i].classList.add('selected');
        } else {
          selected.delete(i);
          els.tbody.children[i].classList.remove('selected');
        }
        updateStats();
      }));
      $$('.rep').forEach(inp=>inp.addEventListener('input', updateStats));
      
      // 绑定移动按钮事件
      $$('.move-up').forEach(btn => {
        btn.addEventListener('click', e => {
          const i = +e.target.dataset.i;
          movePlate(i, i - 1);
        });
      });
      
      $$('.move-down').forEach(btn => {
        btn.addEventListener('click', e => {
          const i = +e.target.dataset.i;
          movePlate(i, i + 1);
        });
      });

      updateStats();
      els.btnExport.disabled = plates.length === 0;
      
      if (fileZips.size > 0) {
        const firstFileName = Array.from(fileZips.keys())[0].split('_').slice(0, -1).join('_');
        const loops = Math.max(1, +els.loops.value || 1);
        let totalPlates = 0;

        plates.forEach((p, idx) => {
          const row = els.tbody.children[idx];
          if (!row) return;
          const checked = row.querySelector('.chk').checked;
          const repInput = row.querySelector('.rep');
          const rep = Math.max(0, +repInput.value || 0);
          if (checked && rep > 0) {
            totalPlates += rep;
          }
        });

        const totalTimes = loops * totalPlates;
        const baseName = firstFileName.replace(/\.gcode\.3mf$/i,'').replace(/\.3mf$/i,'');

        els.fileName.placeholder = `${baseName}_${totalTimes}次.自动换盘`;
      }

      els.fileName.addEventListener('keydown', (e) => {
        if (e.key === 'Enter' && !els.fileName.value && els.fileName.placeholder) {
          els.fileName.value = els.fileName.placeholder;
          e.preventDefault();
        }
      });
    }

    function updateStats(){
      const loopsInput = els.loops;
      if (loopsInput.value === '0') {
        loopsInput.value = '';
      }
      const loops = Math.max(1, +loopsInput.value || 1);
      
      let totalSec = 0; 
      let activePlates = 0; 
      let filamentSum = new Map();
      
      plates.forEach((p,idx)=>{
        const row = els.tbody.children[idx];
        if (!row) return;
        
        const checked = row.querySelector('.chk').checked;
        const repInput = row.querySelector('.rep');
        
        if (repInput.value === '0') {
          repInput.value = '';
        }
        const rep = Math.max(0, +repInput.value || 0);
        
        if(checked && rep>0){
          activePlates += rep;
          totalSec += p.timeSec * rep;
          
          p.filaments.forEach(f=>{
            const key = f.id;
            const cur = filamentSum.get(key) || {m:0,g:0,color:f.color, type: f.type || '未知材料'};
            cur.m += (f.used_m||0) * rep; 
            cur.g += (f.used_g||0) * rep; 
            cur.color = f.color;
            cur.type = f.type || '未知材料';
            filamentSum.set(key, cur);
          });
        }
      });
      
      totalSec *= loops; 
      activePlates *= loops;

      let avgSec = 0;
      if (activePlates > 0) {
        avgSec = totalSec / activePlates;
      }

      els.totalTime.textContent = fmtHMS(totalSec);
      els.avgTime.textContent = fmtHMS(avgSec);
      els.plateCount.textContent = String(plates.length);
      els.loopPlates.textContent = String(activePlates);

      els.filamentTags.innerHTML = '';
      const materialMap = new Map();
      [...filamentSum.entries()].forEach(([slot, val]) => {
        const type = val.type || '未知材料';
        if (!materialMap.has(type)) {
          materialMap.set(type, { g: 0, color: val.color });
        }
        const mat = materialMap.get(type);
        mat.g += val.g;
        mat.color = val.color;
      });

      [...materialMap.entries()].forEach(([type, val]) => {
        const totalG = Math.ceil(val.g * loops);
        const div = document.createElement('div');
        div.className = 'tag';
        div.innerHTML = `<span style="width:10px;height:10px;border-radius:50%;display:inline-block;background:${val.color}"></span> ${type} · ${totalG}g`;
        els.filamentTags.appendChild(div);
      });
    }

    els.btnSelectAll.addEventListener('click',()=>{
      $$('.chk').forEach(c=>{c.checked=true}); 
      selected = new Set(plates.map((_,i)=>i));
      $$('tbody tr').forEach(tr => tr.classList.add('selected'));
      updateStats();
    });
    
    els.btnClearAll.addEventListener('click',()=>{
      $$('.chk').forEach(c=>{c.checked=false}); 
      selected.clear();
      $$('tbody tr').forEach(tr => tr.classList.remove('selected'));
      updateStats();
    });
    
    els.loops.addEventListener('input', updateStats);

    els.btnExport.addEventListener('click', async ()=>{
      try{
        if(fileZips.size === 0) return;
        
        const loopsInput = els.loops;
        const loops = Math.max(1, +loopsInput.value || 1);
        
        let queue = [];
        plates.forEach((p,idx)=>{
          const row = els.tbody.children[idx];
          if (!row) return;
          
          const checked = row.querySelector('.chk').checked;
          const repInput = row.querySelector('.rep');
          const rep = Math.max(0, +repInput.value || 0);
          
          if(checked && rep>0){ 
            for(let i=0;i<rep;i++) {
              queue.push(p); 
            }
          }
        });
        
        let finalQueue = []; 
        for(let k=0;k<loops;k++) {
          finalQueue = finalQueue.concat(queue); 
        }
        
        if(!finalQueue.length) {
          throw new Error('未选择任何托盘');
        }

        let merged = ";\n; 合并多个3MF文件生成的G-code\n; 来源文件: " + Array.from(fileZips.keys()).map(k => k.split('_').slice(0, -1).join('_')).join(', ') + "\n;\n";
        
        for(let i=0;i<finalQueue.length;i++){
          const p = finalQueue[i];
          // 查找对应的zip文件
          let fileData = null;
          for(const [key, value] of fileZips.entries()) {
            if(key.startsWith(p.sourceFile)) {
              fileData = value;
              break;
            }
          }
          
          if (!fileData) continue;
          
          const seg = await fileData.zip.file(p.gcodePath).async('string');
          merged += `\n; ==== 托盘 ${i+1} : ${p.name} (来自 ${p.sourceFile}) ====\n` + seg + `\n`;
        }

        const z = new JSZip();
        
        const firstFile = Array.from(fileZips.values())[0];
        const entriesKeys = Object.keys(firstFile.zip.files);
        
        for(const name of entriesKeys){
          if(/Metadata\/plate_\d+\.gcode$/i.test(name) || 
             /Metadata\/plate_\d+\.png$/i.test(name) ||
             /Metadata\/plate_\d+\.json$/i.test(name) ||
             /Metadata\/top_\d+\.png$/i.test(name) ||
             /Metadata\/pick_\d+\.png$/i.test(name)) {
            continue;
          }
          
          const fileData = firstFile.zip.files[name];
          if (fileData.dir) {
            z.folder(name);
          } else {
            const content = await firstFile.zip.file(name).async('arraybuffer');
            z.file(name, content);
          }
        }
        
        z.file('Metadata/plate_1.gcode', merged);
        
        const hash = SparkMD5.hash(merged);
        z.file('Metadata/plate_1.gcode.md5', hash);

        const modelCfg = await firstFile.zip.file('Metadata/model_settings.config').async('text');
        const parser = new DOMParser(); 
        const modelXML = parser.parseFromString(modelCfg,'text/xml');
        const platesXML = [...modelXML.getElementsByTagName('plate')];
        
        if(platesXML.length){
          const p0 = platesXML[0];
          for(let i=platesXML.length-1;i>=1;i--) {
            platesXML[i].remove();
          }
          
          const ensure = (k,v)=>{
            let m = p0.querySelector(`metadata[key="${k}"]`);
            if(!m){ 
              m = modelXML.createElement('metadata'); 
              m.setAttribute('key',k); 
              p0.appendChild(m); 
            }
            m.setAttribute('value', v);
          };
          
          ensure('plater_id','1');
          ensure('plater_name','合并文件');
          ensure('gcode_file','Metadata/plate_1.gcode');
          ensure('thumbnail_file', 'Metadata/plate_1.png');
          ensure('top_file','Metadata/top_1.png');
          ensure('pick_file','Metadata/pick_1.png');
          ensure('pattern_bbox_file','Metadata/plate_1.json');
        }
        
        const xmlStr = new XMLSerializer().serializeToString(modelXML);
        z.file('Metadata/model_settings.config', xmlStr);

        const blob = await z.generateAsync({type:'blob', compression:'DEFLATE', compressionOptions:{level:3}});
        const a = document.createElement('a');

        let totalPlates = 0;
        plates.forEach((p, idx) => {
          const row = els.tbody.children[idx];
          if (!row) return;
          const checked = row.querySelector('.chk').checked;
          const repInput = row.querySelector('.rep');
          const rep = Math.max(0, +repInput.value || 0);
          if (checked && rep > 0) {
            totalPlates += rep;
          }
        });
        const totalTimes = loops * totalPlates;

        let baseName = els.fileName.value || els.fileName.placeholder || 'output';
        baseName = baseName.replace(/\.3mf$/i, '').replace(/\.gcode\.3mf$/i, '');

        const fname = `${baseName}_${totalTimes}次.3mf`;

        a.href = URL.createObjectURL(blob); 
        a.download = fname; 
        a.click(); 
        URL.revokeObjectURL(a.href);
        
      }catch(err){ 
        showErr(err.message||String(err)); 
      }
    });

    // 初始化拖拽排序
    initDragSort();
  </script>
</body>
</html>